home *** CD-ROM | disk | FTP | other *** search
/ MacWorld 1999 September (IDG) / Sep99.iso / Shareware World / Utilities / Text Processing / Alpha / Tcl / SystemCode / textFill.tcl < prev    next >
Encoding:
Text File  |  1999-05-05  |  17.6 KB  |  573 lines  |  [TEXT/ALFA]

  1. ####################################################################
  2. # Much by Vince Darley.
  3. #                                    created: 26/11/96 {7:08:34 pm} 
  4. #                                last update: 05/05/1999 {18:20:37 PM}
  5. #  Author: Vince Darley
  6. #  E-mail: <darley@fas.harvard.edu>
  7. #    mail: Division of Applied Sciences, Harvard University
  8. #          Oxford Street, Cambridge MA 02138, USA
  9. #     www: <http://www.fas.harvard.edu/~darley/>
  10. #  
  11. ####################################################################
  12.  
  13. ## 
  14.  # Here's a brief explanation of the smart fillParagraph routines
  15.  # 
  16.  # 'fillParagraph' 
  17.  # 
  18.  #  If there's a selection, then fill all paragraphs in that selection.  If
  19.  #  not then fill the paragraph surrounding the insertion point.  The
  20.  #  definition of a 'paragraph' may be mode dependent (see paraStart,
  21.  #  paraFinish)
  22.  #       
  23.  # 'fillOneParagraph'
  24.  # 
  25.  #  Fills the single paragraph surrounding the insertion point.  If called
  26.  #  with parameter '0', it doesn't bother to remember where the insertion
  27.  #  point was, which makes multiple paragraph fills quicker when called by
  28.  #  'fillParagraph'.  Works for mode-dependent definitions of paragraphs and
  29.  #  for commented out text (such as this paragraph here!).  
  30.  #       
  31.  # 'rememberWhereYouAre'
  32.  # 
  33.  #  Given the start of a paragraph and the point to remember, this returns a
  34.  #  record which must be passed to the following function so that it can
  35.  #  find the spot later, even after the paragraph has had
  36.  #  space/tabs/new-lines meddled with.  An optional last argument is a list
  37.  #  of other characters quoted so they are regexp insensitive, which should
  38.  #  also be ignored.  This is used so we can remember positions in text
  39.  #  which has cosmetic characters on the left/right which are not wrapped
  40.  #  (such as the hashes to the left here!).
  41.  #       
  42.  # 'goBackToWhereYouWere'
  43.  # 
  44.  #  Given the beginning and end of a selection, and a previous record, where
  45.  #  the beginning, and record correspond to a previous call of
  46.  #  'rememberWhereYouAre', this procedure will move the insertion point to
  47.  #  the correct place.
  48.  #       
  49.  # 'paraStart'
  50.  #    
  51.  #  Finds the start of the paragraph containing the insertion point.
  52.  #       
  53.  # 'paraFinish'
  54.  # 
  55.  #  Finds the end of the paragraph containing the insertion point.
  56.  ##
  57.     
  58. proc fillParagraph {} {
  59.     if {[pos::compare [getPos] == [selEnd]]} {
  60.     fillOneParagraph
  61.     } else {    
  62.     set start [getPos]
  63.     set end [selEnd]
  64.     set p $start
  65.     while {[pos::compare $p < $end] && [pos::compare $p < [maxPos]]} {
  66.         goto $p
  67.         set p [fillOneParagraph 0 $start $end]
  68.     }
  69.     goto $start
  70.     }
  71. }
  72.  
  73. proc rememberWhereYouAre {startPara pos endPara {commentReg ""}} {
  74.     set start [pos::math $pos -20]
  75.     if {[pos::compare $start < $startPara]} {
  76.     set start $startPara
  77.     }
  78.     set __g_remember_str [getText $start $pos]
  79.     if {[string length [string trim $__g_remember_str]] < 3} {
  80.     # there wasn't much to remember; try the other way
  81.     set end [pos::math $pos +20]
  82.     if {[pos::compare $end > $endPara]} {
  83.         set end $endPara
  84.     }
  85.     set __g_remember_str [getText $pos $end]
  86.     set __g_remember_dir 0
  87.     } else {
  88.     set __g_remember_dir 1
  89.     }
  90.     
  91.     set __g_remember_str [quote::Regfind $__g_remember_str]
  92.     regsub -all "\[ \t\r\n${commentReg}\]+" $__g_remember_str \
  93.       {[ \t\r\n${commentReg}]+} __g_remember_str
  94.     return [list $__g_remember_str $__g_remember_dir]
  95. }
  96.  
  97. proc goBackToWhereYouWere {start end memory} {
  98.     if {[lindex $memory 0] != "" } {
  99.     regexp -indices ".*([lindex $memory 0]).*" [getText $start $end] \
  100.       "" submatch
  101.     if {[info exists submatch]} {
  102.         set p [pos::math $start + [lindex $memory 1] + \
  103.           [lindex $submatch [lindex $memory 1]]]
  104.     } else {
  105.         set p $end
  106.     }
  107.     goto [expr {[pos::compare $p >= $end] ? [pos::math $end - 1] : $p}]
  108.     } else {
  109.     goto $start
  110.     }
  111. }
  112.  
  113. ## 
  114.  # -------------------------------------------------------------------------
  115.  #     
  116.  #    "getLeadingIndent" --
  117.  #    
  118.  #  Find the indentation of the line containing 'pos', and convert it to a
  119.  #  minimal form of tabs followed by spaces.  If 'size' is given, then the
  120.  #  variable of that name is set to the length of the indent.  Similarly
  121.  #  'halftab' can be set to half a tab. 
  122.  # -------------------------------------------------------------------------
  123.  ##
  124. proc getLeadingIndent { pos {size ""} {halftab ""} } {
  125.     # get the leading whitespace of the current line
  126.     set res [search -s -n -f 1 -r 1 "^\[ \t\]*" [lineStart $pos]]
  127.     
  128.     # convert it to minimal form: tabs then spaces, stored in 'front'
  129.     getWinInfo a
  130.     set sp [string range "              " 1 $a(tabsize) ]
  131.     regsub -all "($sp| +\t)" [eval getText $res] "\t" front
  132.     if { $size != "" } {
  133.     upvar $size ind
  134.     # get the length of the indent
  135.     regsub -all "\t" $front $sp lfront
  136.     set ind [string length $lfront]
  137.     }
  138.     if { $halftab != "" } {
  139.     upvar $halftab ht
  140.     # get the length of half a tab
  141.     set ht [string range "            " 1 [expr {$a(tabsize)/2}]]
  142.     }
  143.     
  144.     return $front
  145. }
  146.  
  147. ## 
  148.  # -------------------------------------------------------------------------
  149.  # 
  150.  # "fillOneParagraph" --
  151.  # 
  152.  #  Fixes: won't put a double-space after abbreviations like 'e.g.', 'i.e.'
  153.  #  
  154.  #  Works around the Alpha 'replaceText' bug.
  155.  # -------------------------------------------------------------------------
  156.  ##
  157. proc fillOneParagraph {{remember 1} {minstart ""} {maxend ""}} {
  158.     global leftFillColumn fillColumn doubleSpaces
  159.  
  160.     set pos [getPos]
  161.     if {[set inComment [text::isInComment $pos ch]]} {
  162.     # Find lines which contain just a comment char, but no actual text
  163.     # (We want to flow the text in the comment in its constituent
  164.     # paragraphs, not as one big block).
  165.     set ch [string trim $ch]
  166.     set chreg [quote::Regfind ${ch}]
  167.     if {$ch == "*"} {
  168.         # We assume it's a C-style comment
  169.         set start [pos::math [lindex [search -s -f 0 -r 1 "^\[ \t\]*(${chreg}+|/\\*+)\[ \t\]*\$" $pos] 1] +1]
  170.         set end [lindex [search -s -f 1 -r 1 "^\[ \t\]*(${chreg}+|\\*+/)\[ \t\]*\$" $pos] 0]
  171.     } else {
  172.         set start [lindex [search -s -n -f 0 -r 1 "^\[ \t\]*(${chreg}+\[ \t\]*${chreg}*\$|\[^${chreg} \t\]|\$)" $pos] 0]
  173.         set end [lindex [search -s -n -f 1 -r 1 "^\[ \t\]*(${chreg}+\[ \t\]*${chreg}*\$|\[^${chreg} \t\]|\$)" $pos] 0]
  174.         # The comment doesn't have a leading/trailing almost blank line
  175.         # Look for any line which is either blank, or starts with a 
  176.         # different character
  177.         if {$start == ""} {
  178.         set start [nextLineStart [lindex [search -s -f 0 -r 1 "^\[ \t\]*(\[^ \t[string index $ch 0]\]|\$)" $pos] 0]]
  179.         } else {
  180.         set start [nextLineStart $start]
  181.         }
  182.         if {$end == ""} {
  183.         set end [lindex [search -s -f 1 -r 1 "^\[ \t\]*(\[^ \t[string index $ch 0]\]|\$)" $pos] 0]
  184.         }
  185.     }
  186.     } else {
  187.     set start [paraStart $pos] 
  188.     if {[pos::compare $start > $pos]} {
  189.         set end [paraFinish $start]
  190.     } else {
  191.         set end [paraFinish $pos]
  192.     }
  193.     }
  194.     # Extra arguments allow us to specify a region in which to operate
  195.     if {$minstart != ""} {
  196.     if {[pos::compare $minstart > $start]} {
  197.         set start $minstart
  198.     }
  199.     }
  200.     if {$maxend != ""} {
  201.     if {[pos::compare $maxend < $end]} {
  202.         set end $maxend
  203.     }
  204.     }
  205.     
  206.     if {$remember} {
  207.     if {$inComment} {
  208.         set memory [rememberWhereYouAre $start $pos $end $chreg]
  209.     } else {
  210.         set memory [rememberWhereYouAre $start $pos $end]
  211.     }
  212.     }
  213.     
  214.     if {$inComment} {
  215.     set text [getText $start [nextLineStart $start]]
  216.     if {[set boxComment [regexp -- "(${chreg}+)\[\r\n\]" $text "" commentSuffix]]} {
  217.         set boxWidth [posX [pos::math [nextLineStart $start] -1]]
  218.     }
  219.     regsub -all -- $chreg $text [string range "   " 1 [string length $ch]] fr
  220.     regexp "^\[ \t\]*" $fr fr
  221.     set left [string length [text::maxSpaceForm $fr]]
  222.     if {$boxComment} {
  223.         set newFillColumn [expr {$boxWidth - $left - [string length $commentSuffix] -2}]
  224.     } else {
  225.         set newFillColumn [expr {$fillColumn - $left}]
  226.     }
  227.     
  228.     if {![regexp "^((\[ \t\]*${chreg}+)\[ \t\]*)" $text "" front commentPrefix]} {
  229.         alertnote "Sorry, I can't yet reflow the text inside this comment."
  230.         return $end
  231.     }
  232.     if {$boxComment} {
  233.         regsub -all "[quote::Regfind $commentSuffix](\r|\n|$)" [getText $start $end] "\\1" text
  234.         regsub -all "(^|\r|\n)[quote::Regfind $commentPrefix]" $text "" text
  235.     } else {
  236.         regsub -all "(^|\r|\n)[quote::Regfind $commentPrefix]" [getText $start $end] "" text
  237.     }
  238.     
  239.     regsub -all "\[ \t\r\n\]+" [string trim $text] " " text
  240.     } else {
  241.     # Get the leading whitespace of the current line and store length in 'left'
  242.     set front [getLeadingIndent $pos left]
  243.     # fill the text
  244.     regsub -all "\[ \t\r\n\]+" [string trim [getText $start $end]] " " text
  245.     set newFillColumn [expr {$fillColumn - $left}]
  246.     }
  247.     
  248.     # turn single spaces at end of sentences into double
  249.     if {$doubleSpaces} {regsub -all {(([^.][a-z]|[^a-zA-Z@]|\\@)[.?!]("|'|'')?([])])?) } $text {\1  } text}
  250.     #     if {$doubleSpaces} {regsub -all {(([^A-Z@]|\\@)[.?!][])'"]?) } $text {\1  } text}
  251.  
  252.     # temporarily adjust the fillColumns
  253.     set ol $leftFillColumn
  254.     set or $fillColumn
  255.     set leftFillColumn 0
  256.     set fillColumn $newFillColumn
  257.         
  258.     # break and indent the paragraph
  259.     regsub -all " ?\r" "\r[string trimright [breakIntoLines $text]]" "\r${front}" text
  260.     # reset columns
  261.     set leftFillColumn $ol
  262.     set fillColumn  $or
  263.     if {$inComment && $boxComment} {
  264.     global bind::_IndentSpaces
  265.     set newtext ""
  266.     foreach line [split $text "\r\n"] {
  267.         puts stdout [string length $line]
  268.         set pad [string range [set bind::_IndentSpaces] 0 [expr {$boxWidth- [string length $line] -2}]]
  269.         lappend newtext "$line$pad$commentSuffix"
  270.     }
  271.     set text "\r[join [lrange $newtext 1 end] \r]"
  272.     }
  273.     
  274.     # don't replace if nothing's changed
  275.     if {"$text\r" != "\r[getText $start $end]"} {
  276.     # workaround an alpha bug
  277.     if {$remember} { 
  278.         getWinInfo a
  279.         if {[pos::compare [rowColToPos $a(currline) 0] > $start]} { goto $start }
  280.     }
  281.     replaceText $start $end "[string range $text 1 end]\r"
  282.     if {$remember} {
  283.         goBackToWhereYouWere $start [pos::math $start + \
  284.           [string length $text]] $memory
  285.     }
  286.     }
  287.     
  288.     # in case we wish to fill a region
  289.     return $end
  290. }
  291.  
  292. ## 
  293.  # -------------------------------------------------------------------------
  294.  # 
  295.  #    "paraStart"    -- "paraFinish"
  296.  # 
  297.  #  Newly simplified version with fewer regexp '()' pairs.  Also I think it
  298.  #  deals better with TeX comments than the old regexp.
  299.  #     
  300.  #  "Start": It's pretty clear for non TeX modes how this works.  The only
  301.  #  key is that we start at the beginning of the current line and look
  302.  #  back.  We then have a quick check for whether we found that very
  303.  #  beginning (in which case return it) or if not (in which case we have
  304.  #  found the end of the previous paragraph) we move forward a line.
  305.  # 
  306.  #  "Finish": The only addition is the need for an additional check for
  307.  #  stuff which explicitly ends lines.
  308.  #       
  309.  # Results:
  310.  #  The start/finish position of the paragraph containing the given 'pos'
  311.  # 
  312.  # --Version--Author------------------Changes-------------------------------
  313.  #    1.1     <darley@fas.harvard.edu> Cut down on '()' pairs
  314.  #    1.2     Vince - March '96          Better filling for TeX tables ('hline')
  315.  #    1.3     Johan Linde - May '96   Now sensitive to HTML elements
  316.  #    1.4     <darley@fas.harvard.edu> Handle Tcl lists, top of file fix.
  317.  # -------------------------------------------------------------------------
  318.  ##
  319. proc paraStart {pos} {
  320.     global mode 
  321.     global ${mode}::startPara
  322.     if {[pos::compare $pos == [maxPos]]} {set pos [pos::math $pos - 1]}
  323.     set pos [lineStart $pos]
  324.     if {[info exists ${mode}::startPara]} {
  325.     set startPara [set ${mode}::startPara]
  326.     } else {
  327.     switch -- $mode {
  328.         "TeX" -
  329.         "Bib" {
  330.         global texParaCommands
  331.         set startPara {^[ \t]*$|\\\\[ \t]*$|(^|[^\\])%|\\h+line[ \t]*$|\$\$[ \t]*$|^[ \t]*(\\(}
  332.         append startPara $texParaCommands {)(\[.*\]|\{.*\}|•)*[ \t]*)+$}
  333.         } 
  334.         "HTML" {
  335.         global htmlParaCommands
  336.         set startPara {^[ \t]*$|</?(}
  337.         append startPara $htmlParaCommands {)([ \t\r]+[^>]*>|>)}
  338.         }
  339.         default {
  340.         set startPara {^([ \t]*|([\\%].*))$}
  341.         }
  342.     }
  343.     }
  344.  
  345.     set res [search -s -n -f 0 -r 1 -l [minPos] -- "$startPara" $pos]
  346.     if {![llength $res] || $res == "0 0" } {
  347.     # bug work-around.  Alpha fails to match '^' with start of file.
  348.     return [lineStart [lindex [search -s -f 1 -r 1 "\[^ \t\r\n\]" [minPos]] 0]]
  349.     } elseif {[pos::compare [lindex $res 0] == $pos]} {
  350.     return $pos
  351.     } else {
  352.     return [nextLineStart [lindex $res 0]]
  353.     }
  354.     
  355. }
  356.  
  357. proc paraFinish {pos} {
  358.     global mode
  359.     global ${mode}::endPara
  360.     set pos [lineStart $pos]
  361.     set end [maxPos]
  362.     if {[info exists ${mode}::endPara]} {
  363.     set endPara [set ${mode}::endPara]
  364.     } else {
  365.     switch -- $mode {
  366.         "TeX" -
  367.         "Bib" {
  368.         global texParaCommands
  369.         set endPara {^[ \t]*$|(^|[^\\])%|\$\$[ \t]*$|^[ \t]*(\\(}
  370.         append endPara $texParaCommands {)(\[.*\]|\{.*\}|•)*[ \t]*)+$}
  371.         } 
  372.         "HTML" {
  373.         global htmlParaCommands
  374.         set endPara {^[ \t]*$|</?(}
  375.         append endPara $htmlParaCommands {)([ \t\r\n]+[^>]*>|>)}
  376.         }
  377.         default {
  378.         set endPara {^([ \t]*|([\\%].*))$}
  379.         }
  380.     }
  381.     }
  382.     
  383.     set res [search -s -n -f 1 -r 1 -l $end -- "$endPara" $pos]
  384.     if {![string length $res]} {return $end}
  385.     set cpos [lineStart [lindex $res 0]]
  386.     if {[pos::compare $cpos == $pos]} {
  387.     return [nextLineStart $cpos]
  388.     }
  389.     # A line which ends in '\\', '%...', '\hline', '\hhline'
  390.     # signifies the end of the current paragraph in TeX mode
  391.     # (the above checked for beginning of the next paragraph).
  392.     if { $mode == "TeX" || $mode == "Bib" } {
  393.     set res2 [search -s -n -f 1 -r 1 -l $end {((\\\\|\\h+line)[ \t]*|[^\\]%.*)$} $pos]
  394.     if {[string length $res2]} {
  395.         if {[pos::compare [lindex $res2 0] < $cpos] } {
  396.         return [nextLineStart [lindex $res2 0]]
  397.         }
  398.     }
  399.     }
  400.  
  401.     return $cpos
  402.     
  403. }
  404.  
  405. proc selectParagraph {} {
  406.     set pos [getPos]
  407.     set start [paraStart $pos] 
  408.     set finish [paraFinish $pos]
  409.     goto $start
  410.     select $start $finish
  411. }
  412.  
  413. proc sentenceParagraph {} {
  414.     set pos [getPos]
  415.     set start [paraStart $pos] 
  416.     set finish [paraFinish $pos]
  417.     
  418.     set t [string trim [getText $start $finish]]
  419.     set period [regexp {\.$} $t]
  420.     regsub -all "\[ \t\r\n\]+" $t " " text
  421.     regsub -all {\. } $text "Δ" text
  422.     set result ""
  423.     foreach line [split [string trimright $text {.}] "Δ"] {
  424.     if {[string length $line]} {
  425.         append result [breakIntoLines $line] ".\r"
  426.     }
  427.     }
  428.     if {!$period && [regexp {\.\r} $result]} {
  429.     set result [string trimright $result ".\r"]
  430.     append result "\r"
  431.     }
  432.     if {$result != [getText $start $finish]} {
  433.     replaceText $start $finish $result
  434.     }
  435.     goto $pos
  436. }
  437.  
  438. proc getEndpts {} {
  439.     if {[pos::compare [getPos] == [selEnd]]} {
  440.     set start [getPos]
  441.     set finish [getMark]
  442.     if {[pos::compare $start > $finish]} {
  443.         set temp $start
  444.         set start $finish
  445.         set finish $temp
  446.     }
  447.     } else {
  448.     set start [getPos]
  449.     set finish [selEnd]
  450.     }
  451.     return [list $start $finish]
  452. }
  453.  
  454.  
  455. proc fillRegion {} {
  456.     global leftFillColumn
  457.     set ends [getEndpts]
  458.     set start [lineStart [lindex $ends 0]]
  459.     set finish [lindex $ends 1]
  460.     goto $start
  461.     set text [fillText $start $finish]
  462.     replaceText $start $finish [format "%$leftFillColumn\s" ""] $text "\r"
  463. }
  464.     
  465. proc wrapParagraph {} {
  466.     set pos [getPos]
  467.     set start [paraStart $pos] 
  468.     set finish [paraFinish $pos]
  469.     goto $start
  470.     wrapText $start $finish
  471.     goto $pos
  472. }
  473.  
  474. proc wrapRegion {} {
  475.     set ends [getEndpts]
  476.     set start [lineStart [lindex $ends 0]]
  477.     set finish [lindex $ends 1]
  478.     if {[pos::compare $start == $finish]} {
  479.     set finish [maxPos]
  480.     }
  481.     wrapText $start $finish
  482. }
  483.     
  484.  
  485.  
  486. # Remove text from window, transform, and insert back into window.
  487. proc fillText {from to} {
  488.     global doubleSpaces
  489.     set text [getText $from $to]
  490.     regexp "^\[ \t\]*" $text front
  491.     regsub -all "\[ \t\n\r\]+" [string trim $text] " " text
  492.     if {$doubleSpaces} {regsub -all {(([^.][a-z]|[^a-zA-Z@]|\\@)[.?!]("|'|'')?([])])?) } $text {\1  } text}
  493.     regsub -all " ?\[\r\n\]" [string trimright [breakIntoLines $text]] "\r${front}" text
  494.     return $front$text
  495. }
  496.  
  497. proc paragraphToLine {} {
  498.     global fillColumn
  499.     global leftFillColumn
  500.     set fc $fillColumn
  501.     set lc $leftFillColumn
  502.     set fillColumn 10000
  503.     set leftFillColumn 0
  504.     fillRegion
  505.     set fillColumn $fc
  506.     set leftFillColumn $lc
  507. }
  508.  
  509. proc lineToParagraph {} {
  510.     global fillColumn
  511.     global leftFillColumn
  512.     set fc $fillColumn
  513.     set fillColumn 75
  514.     set lc $leftFillColumn
  515.     set leftFillColumn 0
  516.     fillRegion
  517.     set fillColumn $fc
  518.     set leftFillColumn $lc
  519. }
  520.  
  521.  
  522. #set sentEnd {[.!?](\r|\n| +)}
  523. set sentEnd {(\r\r|\n\n|[.!?](\r|\n| +))}
  524. set sentBeg {[\r\n ][A-Z]}
  525.  
  526. proc nextSentence {} {
  527.     global sentBeg sentEnd
  528.     if {![catch {search -s -f 1 -r 1 $sentEnd [getPos]} mtch]} {
  529.     if {![catch {search -s -f 1 -r 1 -i 0 $sentBeg [pos::math [lindex $mtch 1] - 1]} mtch]} {
  530.         goto [pos::math [lindex $mtch 0] + 1]
  531.     }
  532.     }
  533. }
  534.  
  535.  
  536. proc prevSentence {} {
  537.     global sentBeg sentEnd
  538.     if {[catch {search -s -f 0 -r 1 $sentBeg [pos::math [getPos] - 2]} mtch]} return
  539.     if {![catch {search -s -f 0 -r 1 $sentEnd [lindex $mtch 1]} mtch]} {
  540.     if {![catch {search -s -f 1 -r 1 -i 0 $sentBeg [pos::math [lindex $mtch 1] - 1]} mtch]} {
  541.         goto [pos::math [lindex $mtch 0] + 1]
  542.     }
  543.     }
  544. }
  545.  
  546. #===============================================================================
  547. # Called by Alpha to do "soft wrapping"
  548. proc softProc {pos start next} {
  549.     global leftFillColumn
  550.     goto $start
  551.     set finish [paraFinish $start]
  552.     set text [fillText $start $finish]
  553.     if {"${text}\r" != [getText $start $finish]} {
  554.     replaceText $start $finish [format "%$leftFillColumn\s" ""] $text "\r"
  555.     return 1
  556.     } else {
  557.     return 0
  558.     }
  559. }
  560.  
  561. proc dividingLine {} {
  562.     global mode
  563.     global ${mode}modeVars
  564.     if {[info exists ${mode}modeVars(prefixString)]} {
  565.     set a [string trim [set ${mode}modeVars(prefixString)]]
  566.     } else {
  567.     set a "#"
  568.     }
  569.     insertText "${a}===============================================================================\r"
  570. }
  571.